除了通过指针传递参数之外, 程序通常使用指针变量来动态分配内存. 这种动态内存分配可以让c语言程序在运行时请求更多内存, 指针变量存储了动态分配内存空间的地址. 程序通常动态分配内存以调整特定运行的数组大小.
动态内存分配为程序提供了灵活性:
- 在运行时之前不需要知道数组或者其他数据结构的大小.(比如依赖用户输入的大小)
- 需要允许各种输入大小(非固定大小)
- 可以准确分配在运行时所需要的数据结构大小(避免浪费存储容量)
- 在程序运行时增长或者缩小内存分配的大小, 可以在需要时分配更多的空间, 在不需要时去释放空间.
2.4.1. Heap Memory
程序内存空间的每一个字节都有一个与之对应的地址. 程序运行所需的一切都在其内存空间中,不同类型的实体驻留在程序内存空间的不同部分. 举个例子, code(代码段)包换程序的指令, 全局变量存储在 data (数据段)中, 局部变量和参数占据 stack (栈)的空间, 动态内存来自 heap (堆)中. 因为栈和堆在运行时增长(随着函数的调用和返回以及动态内存的分配和释放), 它们通常在程序的地址空间内相距很远, 从而为程序运行留下大量的可用空间.
动态分配内存来自于程序地址空间的堆内存. 当程序在运行时动态的申请内存, 堆提供了一块内存,其地址必须分配给指针变量。
Figure 1 以栈上的指针变量(ptr
)为例说明了正在运行的程序的内存部分, 该变量存储动态分配的堆内存的地址(它指向堆内存).
Figure 1. 栈上的指针指向从堆中分配的内存块.
值得注意的重点是堆空间是匿名内存, 其中"匿名"意味着堆中的地址没有与之绑定的变量名. 声明一个命名的变量会被分配在栈上(局部变量)或者出现在程序内存布局中的数据段(全局变量). 一个局部指针变量或全局指针变量可以存储匿名堆内存位置的地址(例如, 栈上的局部指针变量可以指向堆内存), 并且解引用此指针可以对堆中的数据进行操作.
2.4.2. malloc and free
malloc 和 free 都是C标准库(stdlib
)中的函数, 程序可以调用它们用来分配和释放在堆中的内存. 堆内存必须由C程序显式分配(malloc)和释放(free).
为了分配堆中的内存, 调用 malloc
, 传入要分配堆中的连续内存的字节总数. 使用 sizeof
操作符 计算要请求的字节数. 举个例子, 为了在堆上分配空间去存储一个整型, 程序可以进行如下调用:
// Determine the size of an integer and allocate that much heap space.
malloc(sizeof(int));
malloc
函数向调用函数返回分配的堆中内存的基地址(首地址, 或者在出错的情况下返回 NULL
). 这是一个完整的示例程序,其中调用 malloc
去分配堆内存空间存储一个 int
值:
#include <stdio.h>
#include <stdlib.h>
int main(void) {
int *p;
p = malloc(sizeof(int)); // allocate heap memory for storing an int
if (p != NULL) {
*p = 6; // the heap memory p points to gets the value 6
}
}
malloc
函数返回 void *
类型(万能指针), 这表示指向非指定类型(或任何类型)的通用指针. 当程序调用 malloc
并且将结果分配给指针变量时, 程序会将分配的内存与指针变量类型相关联.
有时候你可以看到调用 malloc
并将返回的类型显式地从 void *
转化为对应的指针类型. 例如:
p = (int *) malloc(sizeof(int));
在 malloc
前的 (int *)
告诉编译器把 malloc
返回的 void *
当作 int *
类型(它将 malloc
返回的类型转换为 int *
). 我们将会 type recasting and the void *
type章节进行更详细的讨论.
如果没有足够的堆内存满足请求分配的字节数, malloc
调用会失败. 通常来说, malloc
失败表示程序出现错误比如向 malloc
传递非常大的请求,传递负数字节, 或者在无限循环中调用 malloc
并耗尽堆内存. 因为调用 malloc
可能失败, 在指针值进行解引用前, 你应该总是对它的返回值进行空值测试(表明 malloc
失败). 对空指针进行解引用会导致程序崩溃!举个例子:
int *p;
p = malloc(sizeof(int));
if (p == NULL) {
printf("Bad malloc error\n");
exit(1); // exit the program and indicate error
}
*p = 6;
当程序不再需要它在堆上通过 malloc
动态分配的内存时, 它可以通过调用 free
函数显式释放内存. 在调用 free
之后把指针的值设置为NULL
也是一个好主意, 这样如果程序中的错误导致在调用free
之后意外取消引用, 程序将崩溃而不是释放堆内存后供后续的调用 malloc
来重新分配(todo:考虑这句是否重新翻译). 这种意外的内存引用可能会导致未定义的程序行为, 这通常很难调试, 而空指针解引用将立即失败, 使其成为相对容易查找和修复的错误.
free(p);
p = NULL;
2.4.3. Dynamically Allocated Arrays and Strings
C程序员经常使用动态分配的内存来存储数组. 成功调用 malloc
会分配请求大小的一块连续的的堆内存. 它将这块内存的起始地址返回给调用者, 使返回的地址值适合堆内存中动态分配数组的基地址。
要为元素数组动态分配空间, 给 malloc
传递所需数组中的总字节数.
也就是说, 程序应该从 malloc
请求每个数组元素中的字节总数乘以数组中的元素数. 用 sizeof(<type>) * <number of elements>
这种表达式给 malloc
传递总字节数参数. 举个例子:
int *arr;
char *c_arr;
// allocate an array of 20 ints on the heap:
arr = malloc(sizeof(int) * 20);
// allocate an array of 10 chars on the heap:
c_arr = malloc(sizeof(char) * 10);
在本例中调用malloc
之后, int
指针变量arr
存储堆内存中 20 个连续整数存储位置的数组的基地址, 而c_arr
指针变量存储堆内存中 10 个连续字符存储位置的数组。图 2 描绘了这可能的样子。
图 2. 20元素的整型数组和10元素的字符数组在堆上分配的空间
请注意, 虽然malloc
返回指向堆内存中动态分配空间的指针, 但 C 程序将该指向堆的指针存储在栈上. 指针变量仅包含堆中数组存储空间的基地址(起始地址) .就像静态声明的数组一样,动态分配的数组的内存位置位于连续的内存位置. 虽然对malloc
的单次调用会导致分配所请求字节数的一块内存, 但对malloc
的多次调用 不会 导致连续的堆地址(在大多数系统上). 在上面的示例中, char
数组元素和int
数组元素可能位于堆中相距较远的地址.
在堆空间中给数组动态分配内存中,程序可以通过指针变量来访问数组。由于指针变量的值表示堆中数组的基地址,因此我们可以使用与访问静态声明数组中的元素相同的语法来访问动态分配的数组中的元素。这里是例子:
int i;
int s_array[20];
int *d_array;
d_array = malloc(sizeof(int) * 20);
if (d_array == NULL) {
printf("Error: malloc failed\n");
exit(1);
}
for (i=0; i < 20; i++) {
s_array[i] = i;
d_array[i] = i;
}
printf("%d %d \n", s_array[3], d_array[3]); // prints 3 3
为什么可以使用与访问静态声明数组中的元素相同的语法来访问动态分配的数组中的元素,这可能并不明显。然而, 尽管他们的类型(静态声明与动态内存分配)不同,s_array
和 d_array
的值都表示内存中的数组基址(首地址)。
表1. 静态分配 s_array 与动态分配 d_array 比较
Expression | Value | Type |
---|---|---|
s_array | base address of array in memory | (static) array of int |
d_array | base address of array in memory | int pointer (int *) |
因为两个变量的名称都表示内存中数组的基地址(数组元素首地址),在变量后面的 [i]
的语法对于二者语义相同:[i]
对于内存中数组的相对基地址偏移 i 处的 int 类型存储位置解引用——访问第i个元素(i从0开始).
对于大多数用途,我们建议使用[i]
语法来访问动态分配数组的元素。然而,程序也使用指针解引用语法(*
操作符)来访问数组元素。举个例子,在引用动态分配数组的指针前面放*
可以对指针解引用以访问数组下标为0的元素(数组首元素):
/* these two statements are identical: both put 8 in index 0 */
d_array[0] = 8; // put 8 in index 0 of the d_array
*d_array = 8; // in the location pointed to by d_array store 8
数组 描述了数组中的更多细节, 2.9.4. 指针运算 部分讨论了通过指针变量访问数组元素。
当程序使用完动态分配的数组后,它应该调用free
来释放堆空间。如前所述,我们建议在释放指针后将其设置为NULL
:
free(arr);
arr = NULL;
free(c_arr);
c_arr = NULL;
free(d_array);
d_array = NULL;
堆内存管理,malloc和free
C 标准库实现了 malloc
和 free
,它们是其堆内存管理器的编程接口。调用时, malloc
需要在未分配的堆内存空间中找到一块可以满足请求大小的连续块。堆内存管理器维护堆内存的未分配扩展区的空闲列表,其中每个扩展区指定连续的未分配堆空间块的起始地址和大小。
最初,所有堆内存都是空的,这意味着空闲列表具有由整个堆区域组成的单个范围。程序对 malloc
和 free
进行一些调用后,堆内存可能会变得碎片化(fragmented),这意味着空闲堆空间块与已分配堆空间块散布在一起。堆内存管理器通常保留不同范围的堆空间大小的列表,以能够快速搜索特定大小的空闲范围。此外,它还实现一个或多个策略,用于在可用于满足请求的多个空闲范围中进行选择。
free
函数可能看起来很奇怪,因为它只期望接收要释放的堆空间的地址,而不需要在该地址处释放的堆空间的大小。这是因为 malloc
不仅分配所请求的内存字节,而且还在分配的块之前分配一些额外的字节来存储标头结构。标头存储有关已分配的堆空间块的元数据,例如大小。这样一来,调用 free
只需要将堆内存的地址传递给 free
即可。 free
的实现可以从传递给 free
的地址之前内存中的标头信息中获取要释放的内存大小。
有关堆内存管理的更多信息,请参阅操作系统教科书(例如, OS in Three Easy Pieces中的第 17 章“可用空间管理”涵盖了这些详细信息)
2.4.4. 指向堆内存的指针和函数
当给函数传递动态分配的数组时,指针变量参数的值被传递给函数(也就是把数组的首地址传递给函数)。因此,无论是传递静态声明或动态分配的数组给函数,函数参数都获得了相同的值——内存中数组的首地址。因此,同一个函数可用于相同类型的静态声明和动态分配的数组,并且可以在函数内部使用相同的语法来访问数组元素。参数声明int *arr
和 int arr[]
是等价的。但是,按照惯例,指针语法往往用于使用动态分配的数组调用的函数:
int main(void) {
int *arr1;
arr1 = malloc(sizeof(int) * 10);
if (arr1 == NULL) {
printf("malloc error\n");
exit(1);
}
/* pass the value of arr1 (base address of array in heap) */
init_array(arr1, 10);
...
}
void init_array(int *arr, int size) {
int i;
for (i = 0; i < size; i++) {
arr[i] = i;
}
}
在从init_array
函数返回之前,内存中的内容如图三所示。注意,当main
将arr1
传递给init_array
时,它仅传递数组的首地址(基址)。数组的大块连续内存仍然在堆上,函数可以通过解引用(dereferencing
)指针参数来访问它。它还传递了数组的大小以便让init_array
函数知道有多少个元素可以访问。
图3. 从 init_array 返回之前的内存内容。 main 的 arr1(实参) 和 init_array 的 arr(形参) 变量都指向同一块堆内存。